← snapshot
9766 bytes
import Link from "next/link";
import { VerdictPanel } from "@/components/verdict-panel";
import { api } from "@/lib/api";
import { type DiffRow, diffLines, statOf, withElisions } from "@/lib/diff";
import { governing } from "@/lib/paths";
import type { DecisionDto } from "@/lib/types";
export const dynamic = "force-dynamic";
/** Fetch a path's text at a snapshot, or null when it isn't there. */
async function readAt(
repo: string,
snapshotId: string | null,
path: string,
): Promise<string | null> {
if (!snapshotId) return null;
try {
const snap = await api.snapshot(repo, snapshotId);
const file = snap.files.find((f) => f.path === path);
if (!file) return null;
const blob = await api.blob(repo, file.blob);
return blob.content ?? "";
} catch {
return null;
}
}
export default async function DiffPage({
params,
searchParams,
}: {
params: Promise<{ repo: string }>;
searchParams: Promise<{ from?: string; into?: string; path?: string }>;
}) {
const { repo } = await params;
const q = await searchParams;
const detail = await api.repo(repo);
const into = q.into ?? detail.default_ref;
const from =
q.from ?? detail.refs.map((r) => r.name).find((n) => n !== into) ?? into;
const link = (next: Partial<{ from: string; into: string; path: string }>) => {
const sp = new URLSearchParams();
sp.set("from", next.from ?? from);
sp.set("into", next.into ?? into);
const p = next.path ?? q.path;
if (p) sp.set("path", p);
return `/repos/${repo}/diff?${sp.toString()}`;
};
// The gate's own answer for exactly this promotion — same call the gate
// page makes, so the two can never disagree.
let preview: Awaited<ReturnType<typeof api.promotePreview>> | null = null;
let previewError: string | null = null;
try {
preview = await api.promotePreview(repo, from, into);
} catch (e) {
previewError = e instanceof Error ? e.message : String(e);
}
const changed = preview?.changed ?? [];
const selected = q.path && changed.includes(q.path) ? q.path : changed[0];
const decisions: DecisionDto[] = await api
.decisions(repo)
.then((d) => d.decisions)
.catch(() => []);
const fromHead = detail.refs.find((r) => r.name === from)?.head ?? null;
const intoHead = detail.refs.find((r) => r.name === into)?.head ?? null;
let rows: (DiffRow | null)[] = [];
let stat = { added: 0, removed: 0 };
if (selected) {
const [before, after] = await Promise.all([
readAt(repo, intoHead, selected),
readAt(repo, fromHead, selected),
]);
const all = diffLines(before ?? "", after ?? "");
stat = statOf(all);
rows = withElisions(all);
}
return (
<div>
<div className="jac-row-between">
<div>
<p className="jac-eyebrow">Blast radius</p>
<h1 className="jac-h1">Diff</h1>
<p className="jac-lede">
What promoting <span className="jac-mono">{from}</span> into{" "}
<span className="jac-mono">{into}</span> would change, and which
decisions govern it.
</p>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<div className="jac-seg">
{detail.refs.map((r) => (
<Link key={r.name} href={link({ from: r.name })} data-on={r.name === from}>
{r.name}
</Link>
))}
</div>
<span className="jac-small" style={{ alignSelf: "center" }}>
into <span className="jac-mono">{into}</span>
</span>
</div>
</div>
{previewError ? (
<div className="jac-error-box">{previewError}</div>
) : preview ? (
<div style={{ marginBottom: 16 }}>
<VerdictPanel verdict={preview.verdict} repo={repo} />
{!preview.fast_forward ? (
<p className="jac-small" style={{ marginTop: 8 }}>
Not a fast-forward — promotion would be refused outright, before
the gate is even consulted.
</p>
) : null}
</div>
) : null}
{changed.length === 0 ? (
<div className="jac-empty">No paths differ between these refs.</div>
) : (
<div className="jac-diff">
{/* ---- the cut: which paths changed, and what governs them ---- */}
<aside className="jac-diff-rail">
<div className="jac-panel-title" style={{ padding: "0 2px 8px" }}>
{changed.length} path{changed.length === 1 ? "" : "s"} in the cut
</div>
<ul className="jac-cut">
{changed.map((path) => {
const govs = governing(path, decisions);
const unsettledHere = govs.filter((d) => d.state === "unsettled");
return (
<li key={path}>
<Link
href={link({ path })}
className="jac-cut-item"
data-on={path === selected}
>
<span className="jac-cut-path">{path}</span>
{unsettledHere.length > 0 ? (
<span className="jac-cut-flag" title="governed by an unsettled decision">
●
</span>
) : govs.length > 0 ? (
<span
className="jac-cut-flag"
data-settled="true"
title="governed, and settled"
>
●
</span>
) : (
<span className="jac-cut-flag" data-ungoverned="true" title="ungoverned">
○
</span>
)}
</Link>
</li>
);
})}
</ul>
<p className="jac-small" style={{ marginTop: 12, lineHeight: 1.6 }}>
<span style={{ color: "var(--jac-blocked)" }}>●</span> unsettled
decision governs this path
<br />
<span style={{ color: "var(--jac-ok)" }}>●</span> governed and
settled
<br />
<span style={{ opacity: 0.6 }}>○</span> ungoverned — nothing was
declared
</p>
</aside>
{/* ---- the cloth: the file, thread by thread ---- */}
<section className="jac-panel">
<div className="jac-panel-head">
<span className="jac-mono" style={{ fontSize: 12.5 }}>
{selected}
</span>
<span className="jac-spacer" />
<span className="jac-diff-stat">
<span className="jac-diff-add">+{stat.added}</span>
<span className="jac-diff-del">−{stat.removed}</span>
</span>
</div>
{/* the shape of the change, as cloth */}
<div className="jac-diff-strip" aria-hidden="true">
{rows.map((row, i) =>
row === null ? null : (
<span
key={`strip-${String(i)}`}
data-kind={row.kind}
style={{ flex: 1 }}
/>
),
)}
</div>
{selected ? (
<div className="jac-diff-body">
{rows.map((row, i) =>
row === null ? (
<div key={`gap-${String(i)}`} className="jac-diff-gap">
⋯
</div>
) : (
<div key={`row-${String(i)}`} className="jac-diff-row" data-kind={row.kind}>
<span className="jac-diff-ln">{row.a ?? ""}</span>
<span className="jac-diff-ln">{row.b ?? ""}</span>
<span className="jac-diff-mark">
{row.kind === "add" ? "+" : row.kind === "del" ? "−" : " "}
</span>
<span className="jac-diff-text">{row.text || " "}</span>
</div>
),
)}
</div>
) : null}
{selected ? (
<div className="jac-panel-foot">
{(() => {
const govs = governing(selected, decisions);
if (govs.length === 0) {
return (
<span>
Nothing governs this path. The gate will admit it and
report that it verified nothing.
</span>
);
}
return (
<>
<span>governed by</span>
{govs.map((d) => (
<Link
key={d.id}
href={`/repos/${repo}/decisions/${d.id}`}
className={`jac-tag${d.state === "unsettled" ? " jac-tag--warn" : ""}`}
style={{ textDecoration: "none" }}
>
{d.state === "unsettled" ? "⚠ " : ""}
{d.title.length > 40 ? `${d.title.slice(0, 40)}…` : d.title}
</Link>
))}
</>
);
})()}
</div>
) : null}
</section>
</div>
)}
<p className="jac-small" style={{ marginTop: 20 }}>
<Link href="/">← let Jackie walk you through this instead</Link>
</p>
</div>
);
}